從 Day11 開第一台 EC2,到昨天接上自動部署,EC2 這條路終於走完了!
不過網站上線不是結束。Server 是我們自己的,壞掉了不會有人通知我們,也不會自己更新成新版本。今天整理一份上線之後的檢查清單,分成 logs、監控、安全和費用四個部分,每一項都附上我的 side project 實際的狀況,順便看看我自己漏了哪些ㄅ!
網站出問題時,最怕的是不知道該從哪裡查起。先把這張表記下來:
| 想知道的事 | 去哪裡看 | 保留多久 |
|---|---|---|
| 部署有沒有成功、卡在哪一步 | GitHub repo 的 Actions Tab | 預設 90 天 |
| EC2 收到了哪些指令、印出了什麼 | Systems Manager 的 Run Command,Command history Tab | 30 天 |
| Next.js 有沒有報錯 | EC2 上的 sudo docker logs --tail 100 my-app |
到 container 被刪掉為止 |
| Caddy 或憑證有沒有問題 | EC2 上的 sudo journalctl -u caddy --since "1 hour ago" |
到 journald 的空間用完為止,滿了會刪最舊的 |
| 誰在 AWS 上改了什麼設定 | CloudTrail 的 Event history | 90 天,不用錢 |
docker logs 那一行要特別注意:Day25 的 deploy.yml 每次部署都會刪掉舊的 container,它的 logs 也會一起消失。所以想查「部署之前」發生了什麼,要在部署之前先看。
Docker 預設會把 container 的 logs 寫成一個 JSON 檔,而且在文件寫得很清楚:預設是不會被新的替換掉(By default, no log-rotation is performed)。所以檔案只會一直變大,直到 container 被刪掉為止。
像之前我有過 container 有3個月都沒有重新部署,container 的 JSON 檔就已經有 16MB、15 萬行。而且這還是在沒什麼人使用的情況下,如果程式出錯、一直狂印錯誤訊息或是使用者上來之後,8GB 的硬碟也可能被它吃光。
因此,文件推薦改用 local 這個格式,它預設就會替換,每個 container 最多保留約 100MB(5 個 20MB 的檔案,而且會壓縮)。在 EC2 上新增一個 Docker 的設定檔:
sudo tee /etc/docker/daemon.json > /dev/null <<'EOF'
{
"log-driver": "local"
}
EOF
cat /etc/docker/daemon.json
sudo systemctl restart docker
有兩點要注意:
cat 確認內容。deploy.yml 每次部署都會重建 container,所以下一次部署之後就會生效。可以用 sudo docker inspect --format '{{.HostConfig.LogConfig.Type}}' my-app 確認,看到 local 就對了。至於 Caddy 的 logs,是交給系統的 journald 管理。journald 預設最多使用檔案系統的 10%(上限 4GB),滿了會自動刪掉最舊的,所以不用另外處理。像我的 side project 的 journald 已經用了 566MB,大部分是系統服務的訊息,Caddy 一天只有十幾行。
EC2 開起來之後,CloudWatch 就會免費記錄幾個基本指標:CPU、網路流量每 5 分鐘一筆,狀態檢查(status check)每 1 分鐘一筆。在 EC2 的 Console 點進自己起的 server,切到 Monitoring(監控) Tab 就看得到。
不過記憶體和硬碟用了多少,CloudWatch 預設沒有記錄,要另外安裝 CloudWatch agent,而且會算成自訂指標另外收費。小網站可以偶爾用 Session Manager 連進去自己看:
free -m
df -h /

一般狀態檢查分成兩種:
CloudWatch 每個月有 10 個 Alarm 免費,SNS 每個月前 1,000 封 email 也免費,一個人用完全夠。
不過剛剛設定的 system/instance Alarm,不會去檢查網站是否正常回應 HTTP 請求。因為機器本身都還是健康的, container 一直重啟或是 Caddy 停了,上面那個 Alarm 都不會響;Day25 的 smoke test 也只有在部署的時候檢查一次。
想從外面定期檢查網站的話,AWS 的做法是 Route 53 的 health check:每隔一段時間從世界各地連到網站,連不上就透過 CloudWatch Alarm 寄信。價格是每個 health check 每月 0.5 美元起,HTTPS 這類選項另外加價(AWS 上的端點有一些免費額度,細節以價格頁為準);另外有兩個小地方要注意:Alarm 要建在美東(us-east-1),HTTPS 的檢查不會驗證憑證,所以憑證過期它也不會發現。
就算你的 DNS 設在 Cloudflare 的話也可以用這個方式,而 Cloudflare 自家的 Health Checks,則需要 Pro 以上方案,Free 方案沒有提供。
如果開帳號時還沒設預算通知的話,可以到 Billing and Cost Management 左邊選單的 Budgets 按 Create budget,用範本建立一個每月預算,填上金額和 email 就好。一般的預算通知是免費的。
Budgets 會在帳務資料更新後判斷費用是否超過門檻,資料通常每 8~12 小時更新一次,所以它不是即時的扣款通知,也不會因為寄了信就自動停掉資源。是否把 credit 納入計算,也要確認預算的費用設定。
可以用以下這張表整理 check 一下,最後一欄就是以我自己 side project 的狀況為例:
| 檢查項目 | 在哪裡看 | 我的 side project |
|---|---|---|
| Security group 只開 80、443 | EC2 的 Security Groups,看 Inbound rules | 有,只開 80、443 |
| IMDSv2 是 Required | EC2 點進執行個體,Details 裡的 IMDSv2 欄位 | 有,使用 AL2023 AMI 建立時的預設設定 |
| root 和 IAM 使用者都開了 MFA | IAM 的 Users,以及右上角帳號選單裡的 Security credentials | 有 |
| 沒有長期有效的 access key | IAM 的 Users → 使用者 → Security credentials Tab | 沒有,7 月建的 access key 還在用 |
| ECR 的 tag 不能覆寫 | ECR 的 repo 設定 | 沒有,還是 Mutable |
| ECR 會掃描 Image 的弱點 | ECR 的 Private registry 設定 | 沒有 |
| ECR 會自動清掉舊版本 | ECR repo 的 Lifecycle policy | 沒有,8 個版本都還在 |
| 不能直接 push 到 main | GitHub repo 的 Settings → Branches 或 Rules | 沒有 |
| actions 有新版本會提醒 | Dependabot | 沒有,還停在舊版,每次執行都有 Node 20 淘汰的警告 |
| 作業系統有更新 | dnf check-release-update |
沒有,開機 11 週都沒更新,新版本已經出了 12 個 |
| Caddy 有更新 | caddy version |
沒有,還是 2.11.4,最新是 2.11.7 |
前三項只要確認一下,下面說明需要動手的幾項。
這個系列從 Day5 就用 aws login 拿短期憑證,照著做的話應該沒有 access key。而我自己的 side project 則是,建了一組 access key 放在自己開發用的電腦上,到現在都還是 Active。長期有效的 key 外洩了就能一直用,能換成 aws login 就換;暫時換不掉,至少看一下 Security credentials 裡的「Last used」,用不到的就停用、刪除。
tag 不能覆寫:Day25 開始,每個版本都用 commit SHA 當 tag,本來就不需要覆寫。到 ECR 點進你的 repo,在 repo 的設定(Edit)把 Image tag mutability 改成 Immutable,之後同一個 tag 就推不上去了,可以避免同一個 tag 被後來推送的 Image 覆寫。
這個會影響 Day25 保留的手動部署功能:按鈕雖然還在,但直接重跑已經推送過的 commit,會在 push 那一步失敗,因為相同的 SHA tag 已經存在。之後要部署新版本,就用新的 commit;要撤銷程式修改,則沿用 Day25 的 git revert,讓 workflow 重新 build 撤銷後的程式碼。
掃描弱點:ECR 的基本掃描(Basic scanning)不用錢,打開之後每次 push 都會檢查 Image 裡的作業系統套件有沒有已知的漏洞(CVE),結果會顯示在 Image 的詳細頁面。設定在 ECR 左邊選單 Private registry 底下的掃描設定,確認掃描類型是 Basic scanning,把 scan on push 打開、篩選條件填 *(所有 repo)就好。Basic scanning 不涵蓋 npm 套件,所以 Next.js 和其他 npm 相依套件的漏洞仍要另外檢查。
自動清掉舊版本:在 repo 中的 Lifecycle policy 建一條規則,只保留最新的 10 個 Image:Image status 選 Any,Match criteria 選 Image count,數量填 10,Rule action 選 Expire。
寫成 JSON 是這樣:
{
"rules": [
{
"rulePriority": 1,
"description": "Keep only the 10 most recent images",
"selection": {
"tagStatus": "any",
"countType": "imageCountMoreThan",
"countNumber": 10
},
"action": {
"type": "expire"
}
}
]
}
可以先用 preview 看看哪些 Image 會被刪除,再套用規則。規則不是立刻執行,符合條件的 Image 通常會在 24 小時內被清掉;如果有想長期保留的版本,要先調整規則,不能只靠「最新 10 個」來保留它。我的 side project 的 ECR 目前有 8 個版本、共 621MB,每個月不到 0.07 美元,錢不多,但不設的話就會一直累積。
Amazon Linux 2023:作業系統不會自動更新,每台機器都會固定用開機時那個版本的套件來源。
可以用 Session Manager 連進去查:
sudo dnf check-release-update
沒有任何輸出,代表已經是最新版;有新版本的話,會列出版本號和升級指令,例如 dnf upgrade --releasever=2023.12.20260930。照著執行(前面加上 sudo),AWS 文件建議寫出明確的版本號,不要用 --releasever=latest。更新完再檢查要不要重開機:
sudo dnf needs-restarting -r
看到 Reboot should not be necessary. 就不用重開;如果更新到 kernel,就用 sudo reboot 重開機。重開機時網站會中斷一兩分鐘,但 Docker 和 Caddy 都有設定開機啟動,container 也有 --restart unless-stopped,它們會自己回來。
Caddy:我們在 Day22 是直接下載執行檔安裝的,不會跟著 dnf 更新。
所以先用 caddy version 看版本,再到 Caddy 在 GitHub 上的 Releases 頁面對照最新版。
要更新的話,重跑 Day22 下載和 install 的那幾行,再 sudo systemctl restart caddy。Caddy 也有實驗性質的 caddy upgrade 指令,但它只會換掉執行檔,一樣要重新啟動才會生效喔!
Node.js:Dockerfile 用的是 node:22-alpine,GitHub Actions 每次 build 都會重新下載,所以每次部署都會拿到最新的修補版本。
EC2 這條路到這裡就告一段落了!下一篇換一條完全不需要 Server 的路:把 my-app 匯出成靜態檔案,放上 S3,再用 CloudFront 送出去ㄅ!